package com.hmdp.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.entity.Shop;
import com.hmdp.mapper.ShopMapper;
import com.hmdp.service.IShopService;
import com.hmdp.utils.RedisData;
import com.hmdp.utils.RedisUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import static com.hmdp.utils.RedisConstants.*;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author 虎哥
 * @since 2021-12-22
 */
@Service
@Slf4j
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    @Resource
    private ShopMapper shopMapper;
    @Resource
    private RedisUtils redisUtils;
    /**
     * 创建线程池
     */
    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
    /**
     * 根据id查询商铺信息
     * @param id 商铺id
     * @return 商铺详情数据
     */
    @Override
    public Shop queryShopById(Long id) {
        /**
         * 互斥锁解决缓存击穿
         */
//      return   redisUtils.redisMutex( Shop.class,id,CACHE_SHOP_KEY,id2->getById(id2));
     return  redisUtils.queryCachePenetration(CACHE_SHOP_KEY,id,Shop.class,id2->getById(id));
    }
    /**
     * 实现逻辑缓存数据
     */
    public void cacheShopData(Long id,Object o,Long expirationTime){
        RedisData redisData = new RedisData();
        redisData.setData(o);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(expirationTime));
        stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
    }
    /**
     * 实现缓存雪崩
     * 根据id查询商铺信息
     * @param id 商铺id
     * @return 商铺详情数据
     */
    public Shop queryCacheAvalanche(Long id) {
        //通过Id查询商品信息
        String entries = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
        Shop shop1 = JSONUtil.toBean(entries, Shop.class);
        //如果没有就查询数据库
        if (StrUtil.isNotBlank(entries)){
            log.warn("查询的redis");
            return shop1;
        }
        /**
         * 缓存雪崩的业务逻辑(互斥锁)
         * 只允许一个人去查询数据库
         */Shop shop = null;
        //添加互斥锁
        try {
            boolean result = addLock(LOCK_SHOP_KEY+id);
            if (!result){
               //休眠
               Thread.sleep(50);
               //没有查到就循环
               return queryCacheAvalanche(id);
            }
            //查询数据库
            shop = shopMapper.selectById(id);
            log.warn("从数据库中查询到商铺信息：{}",shop.toString());
            if (shop==null){
                /**
                 * todo 缓存穿透
                 * 设置空值到redis中，防止缓存穿透
                 * 当Id查询数据库时,该Id在数据库中不存在！！当请求量大的时候会发生缓存穿透问题
                 */
                stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
                return null;
            }
            //保存到redis
            Map<String, Object> stringObjectMap = BeanUtil.beanToMap(shop);
            stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(stringObjectMap), CACHE_SHOP_TTL, TimeUnit.MINUTES);
            log.warn("查询数据库");
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            //关闭锁
            releaseLock(LOCK_SHOP_KEY+id);
        }
        //返回结果
        return shop;
    }
    /**
     * 添加互斥锁
     */
    public boolean addLock(String  key){
        /**
         * setIfAbsent用法:当第一个的key不存在时，设置key的值为value，并返回true；当第一个key存在时，不设置key的值，返回false
         * 简单来说:当第一个人写入key成功,则下一个人在写入key会失效，除非第一个人的key失效时间到了
         */
        Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        /**
         * 知识点：当两个数据类型不一样的情况下，我们转换时要保证拆箱的对象不为null
         * isTrue:表示当(空和0时返回false) isFalse:表示当(空和0时返回true)
         */
        return BooleanUtil.isTrue(result);

    }
    /**
     * 释放互斥锁
     */
    public void releaseLock(String key){
         stringRedisTemplate.delete(key);
    }

    /**
     * 实现缓存穿透
     * 根据id查询商铺信息
     * @param id 商铺id
     * @return 商铺详情数据
     */
    public Shop queryCachePenetration(Long id) {
        //通过Id查询商品信息
        String entries = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
        Shop shop1 = JSONUtil.toBean(entries, Shop.class);
        //如果没有就查询数据库
        if (StrUtil.isNotBlank(entries)){
            log.warn("查询的redis");
            return shop1;
        }
        //查询数据库
        Shop shop =shopMapper.selectById(id);
        log.warn("从数据库中查询到商铺信息：{}",shop.toString());
        if (shop==null){
            /**
             * todo 缓存穿透
             * 设置空值到redis中，防止缓存穿透
             * 当Id查询数据库时,该Id在数据库中不存在！！当请求量大的时候会发生缓存穿透问题
             */
            stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
            return null;
        }
        //保存到redis
        Map<String, Object> stringObjectMap = BeanUtil.beanToMap(shop);
        stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(stringObjectMap), CACHE_SHOP_TTL, TimeUnit.MINUTES);
        log.warn("查询数据库");
        return shop;
    }
}
